import * as middleware from 'swagger-express-middleware'
import * as express from 'express'
import { SwaggerObject, PartialSwaggerObject } from '../infrastructure/swagger-def'
import { loadFiles } from '../infrastructure/utils'
const { pipe, view, lensPath, path, concat, allPass, assoc, anyPass, pathEq, converge, prop, length, head, tap, lensIndex, pick, over, lensProp, filter, omit, always, ifElse, equals, find, isNil, isArrayLike, identity, split, mergeAll, of, keys, flatten, reduce, curry, is } = require('ramda')
import { getControllerRouteInfos, ControllerRouteInfo } from '../infrastructure/express-decorator'
import { merge } from "lodash/fp"
import { logUtil } from '../infrastructure/logutil'
import { loadAllEventHandlers } from "../infrastructure/event-handler"
import isString = require("lodash/isString")
import env from './env'
import { Response, Request, NextFunction } from 'express'
const { map, chain, ap } = require('pointfree-fantasy');
import { authMiddleware } from '../api/auth/auth.service'
import * as favicon from 'serve-favicon';
import * as ejs from 'ejs';
import socketIo = require('socket.io');

// import { wechatMiddleware } from '../api/wechat/wechat.service'
const Task = require('data.task');
const Maybe = require('data.maybe');

// loadAllControllers :: () -> Task err [Controller]
const loadAllControllers = pipe(
    () => loadFiles(`${__dirname}/../api/**/*.controller.@(ts|js)`, ['**/node_modules/**']),
    map(map(require)), // Task [js]
    map(map(f => keys(f).map(x => f[x]))), // Task [[Controller]]
    map(flatten) // Task [Controller]
);

// loadAllControllerRouteInfos :: () -> Task err [ControllerRouteInfo]
const loadAllControllerRouteInfos: any = pipe(
    loadAllControllers, // Task [Controller]
    map(getControllerRouteInfos)
);

// getSwaggerObjectByControllerRouteInfo :: ControllerRouteInfo -> PartialSwaggerObject
function getSwaggerObjectByControllerRouteInfo(routeInfo: ControllerRouteInfo): PartialSwaggerObject {
    const paths = routeInfo.methods
        .filter(m => !!m.swagger)
        .map(m => ({
            [`${routeInfo.path}${m.path}`]: {
                [m.method]: m.swagger
            }
        }))
        .reduce((ret, m) => Object.assign(ret, m), {})
    return { paths }
}

// loadSwaggerObjectBySwaggerFiles :: () -> Task err PartialSwaggerObject
const loadSwaggerObjectBySwaggerFiles: any = pipe(
    () => loadFiles(`${__dirname}/../api/**/*.swagger.@(ts|js)`, ['**/node_modules/**']),
    map(map(require)),
    map(map(f => keys(f).map(x => f[x]))), // Task [[Controller]]
    map(flatten), // Task [Controller]
    map(reduce(merge, {})),
);

// loadSwaggerObjectByControllerDecorators :: () -> Task err SwaggerObject
const loadSwaggerObjectByControllerDecorators: any = pipe(
    loadAllControllerRouteInfos,
    map(map(getSwaggerObjectByControllerRouteInfo)),
    map(reduce(merge, {})),

);

// loadSwaggerObject :: () -> Task err SwaggerObject
const loadSwaggerObject: any = pipe(
    loadSwaggerObjectBySwaggerFiles,
    ap(map(merge, loadSwaggerObjectByControllerDecorators())),
);

// getSwaggerMiddlewares :: swaggerObject -> app -> Task err app
const applySwaggerMiddlewares = curry((swaggerObj: SwaggerObject, app: express.Application): any => {
    const load = new Task((rej, res) =>
        middleware(swaggerObj, app, function (err, middleware) {
            if (err) rej(err)
            res(middleware)
        })
    )

    const errorForker = (err: any, req: express.Request, res: express.Response, next: express.NextFunction) => {
        // skip.token true && no profile_id -> error
        // skip.token false && no profile_id -> verify token -> next
        // violent patch here, todo more elegant solution ?
        const { skip } = env;

        if (err.message.indexOf('Missing') >= 0 && err.message.indexOf('profileId') >= 0) {
            if (skip.token && skip.token()) {
                err.message = 'Missing "profileId" or Token skipped';
                return next(err);
            }
            else return next();
        }

        return next(err);
    };

    return load.map(middleware => app.use(
        middleware.metadata(),
        middleware.CORS(),
        middleware.files(),
        middleware.parseRequest(),
        middleware.validateRequest()
    ))
})

// setupSwaggerMiddlewares :: app -> Task err app
const setupSwaggerMiddlewares = app => {
    //applyFrontWebRoute(app);
    return loadSwaggerObject()
        .chain(SwaggerObject => applySwaggerMiddlewares(SwaggerObject, app))
}

const resFactory = (data: any): any => Object.assign({ code: 0 }, data ? { data } : {});

// setupErrorHandling :: App -> App
const applyErrorHandlingMiddleware = app =>
    app.use(function (err, req, res, next) {
        const responseError = (error, code) => {
            logUtil.error('Error Occurred!\n', error.stack ? error.stack : error); // todo error definition
            res.status(code || 500).json(Object.assign(
                error.message ? { message: error.message } : {},
                error.i18nMsg ? { i18nMsg: error.i18nMsg } : {},
                error.data ? { data: error.data } : {},
                process.env.NODE_ENV == 'production' ? {} : { debug: error.stack }
            ));
        };

        const handleError = (err) => {
            err.error ? responseError(err.error, err.code) :
                isString(err) ? responseError(new Error(err), 500) :
                    responseError(err, err.code);
        };

        // reject with new Task, can be resolve or reject
        if (err instanceof Task) {
            err.fork(
                err => { handleError(err); },
                value => {
                    if (!res.headersSent) res.send(resFactory(value))
                }
            );
        }
        else handleError(err);
    });

const handlerFactory = (handler: express.RequestHandler): express.RequestHandler =>
    (req: express.Request, res: express.Response, next: express.NextFunction) => {
        logUtil.info({ req: req })

        let result: any = handler(req, res, next);

        if (!result) return;

        const forkTask = (task) => task.fork(
            error => { next(error) },
            value => { if (!res.headersSent) res.json(resFactory(value)) }
            //value => {if (!res.headersSent) res.status(200).send(merge({code:200},value))}
        );

        if (result.then && is(Function, result.then)) { // result instanceof Promise invalid
            result
                .then((value: any) => {
                    if (value instanceof Task) forkTask(value);
                    else if (!res.headersSent) res.send(resFactory(value));
                })
                .catch((error: any) => next(error));
        }
        else if (result instanceof Task) forkTask(result);
        else if (!res.headersSent) res.json(resFactory(result));
    };

// createExpressRoute :: ControllerRouteInfo -> express.Router
const createExpressRoute = (routeInfo: ControllerRouteInfo): express.Router => {
    let router: express.Router = express.Router();
    routeInfo.methods.forEach(m => router[m.method](m.path, ...m.middleware, handlerFactory(m.target[m.key])));
    return router;
};

const applyExpressRoute = (app, routeInfo: ControllerRouteInfo) => {
    app.use(routeInfo.path, ...routeInfo.middleware, createExpressRoute(routeInfo))
};

const applyExpressRouters = app => {
    // applyFrontWebRoute(app);
    applyDefaultRoute(app);
    return loadAllControllerRouteInfos()
        .map((x: ControllerRouteInfo[]) => {
            x.map(routeInfo => applyExpressRoute(app, routeInfo))
            return app
        })
};

const applyDefaultRoute = app => {
    app.get('/', (req: express.Request, res: express.Response) => {
        res.redirect('/swagger')
    })
};
const appStartSocketIo = (app) => {
    //socketIo.apply
    console.log('apprunredis', app);
    return app;
};

const applyFrontWebRoute = app => {
    // app.use(express.static(__dirname + '/../client/src'));
    //favicon配置
    app.use(favicon(__dirname + '/../client/src/dist/images/32fav.ico'));

    //ejs模板文件扩展为html文件
    app.set('views', __dirname + '/src');
    app.engine(".html", ejs.__express);
    app.set("view engine", 'html');

    // send all requests to index.html so browserHistory works
    app.get('*', (req, res) => {
        res.render(__dirname + "/../client/src/index.html");
    });
}

const applyAuthMiddleware = app => app.use(authMiddleware);

interface ModifiedRequest extends Request {
    parameters: any;
};

const mergeRequestParameters = req => pipe(
    converge(
        (params, query, body) => mergeAll([params, query, body]),
        [
            prop('params'),
            prop('query'),
            prop('body')
        ]
    ),
)(req);

const mergeRequestParametersMiddleware = (req: ModifiedRequest, res: Response, next: NextFunction) => {
    req.parameters = mergeRequestParameters(req);
    next();
};


// const applyTrustProxy = (app) => app.set('trust proxy', ['loopback', '10.9.160.99']);

const applyStaticFolder = curry((dir: string, app: any) => app.use(express.static(dir)));

//const applyWechatService = app => app.use(wechatMiddleware);

//const applyEventHandler = app => loadAllEventHandlers().chain(() => attachSocket(sio, env.socket.port, app));
const applyEventHandler = app => loadAllEventHandlers().chain(() => Task.of(app));

const setUpApp: any = pipe(
    pipe(express, Task.of, map(applyStaticFolder('static'))),
    chain(setupSwaggerMiddlewares),
    map(applyAuthMiddleware),
    chain(applyExpressRouters),
    map(applyErrorHandlingMiddleware),
    chain(applyEventHandler)
);

export {
    setUpApp,
    mergeRequestParametersMiddleware
}
